/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.http;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
import org.springframework.security.oauth2.common.exceptions.UserDeniedAuthorizationException;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.ResponseErrorHandler;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import static org.mockito.Mockito.when;
/**
* @author Dave Syer
* @author Rob Winch
*
*/
@RunWith(MockitoJUnitRunner.class)
public class OAuth2ErrorHandlerTests {
@Mock
private ClientHttpResponse response;
@Rule
public ExpectedException expected = ExpectedException.none();
private BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails();
private final class TestClientHttpResponse implements ClientHttpResponse {
private final HttpHeaders headers;
private final HttpStatus status;
private final InputStream body;
public TestClientHttpResponse(HttpHeaders headers, int status) {
this(headers, status, new ByteArrayInputStream(new byte[0]));
}
public TestClientHttpResponse(HttpHeaders headers, int status, InputStream bodyStream) {
this.headers = headers;
this.status = HttpStatus.valueOf(status);
this.body = bodyStream;
}
public InputStream getBody() throws IOException {
return body;
}
public HttpHeaders getHeaders() {
return headers;
}
public HttpStatus getStatusCode() throws IOException {
return status;
}
public String getStatusText() throws IOException {
return status.getReasonPhrase();
}
public int getRawStatusCode() throws IOException {
return status.value();
}
public void close() {
}
}
private OAuth2ErrorHandler handler;
@Before
public void setUp() throws Exception {
handler = new OAuth2ErrorHandler(resource);
}
/**
* test response with www-authenticate header
*/
@Test
public void testHandleErrorClientHttpResponse() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.set("www-authenticate", "Bearer error=foo");
ClientHttpResponse response = new TestClientHttpResponse(headers, 401);
// We lose the www-authenticate content in a nested exception (but it's still available) through the
// HttpClientErrorException
expected.expectMessage("401 Unauthorized");
handler.handleError(response);
}
@Test
public void testHandleErrorWithInvalidToken() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.set("www-authenticate", "Bearer error=\"invalid_token\", description=\"foo\"");
ClientHttpResponse response = new TestClientHttpResponse(headers, 401);
expected.expect(AccessTokenRequiredException.class);
expected.expectMessage("OAuth2 access denied");
handler.handleError(response);
}
@Test
public void testCustomHandler() throws Exception {
OAuth2ErrorHandler handler = new OAuth2ErrorHandler(new ResponseErrorHandler() {
public boolean hasError(ClientHttpResponse response) throws IOException {
return true;
}
public void handleError(ClientHttpResponse response) throws IOException {
throw new RuntimeException("planned");
}
}, resource);
HttpHeaders headers = new HttpHeaders();
ClientHttpResponse response = new TestClientHttpResponse(headers, 401);
expected.expectMessage("planned");
handler.handleError(response);
}
@Test
public void testHandle500Error() throws Exception {
HttpHeaders headers = new HttpHeaders();
ClientHttpResponse response = new TestClientHttpResponse(headers, 500);
expected.expect(HttpServerErrorException.class);
handler.handleError(response);
}
@Test
public void testHandleGeneric400Error() throws Exception {
HttpHeaders headers = new HttpHeaders();
ClientHttpResponse response = new TestClientHttpResponse(headers, 400);
expected.expect(HttpClientErrorException.class);
handler.handleError(response);
}
@Test
public void testHandleGeneric403Error() throws Exception {
HttpHeaders headers = new HttpHeaders();
ClientHttpResponse response = new TestClientHttpResponse(headers, 403);
expected.expect(HttpClientErrorException.class);
handler.handleError(response);
}
@Test
// See https://github.com/spring-projects/spring-security-oauth/issues/387
public void testHandleGeneric403ErrorWithBody() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
ClientHttpResponse response = new TestClientHttpResponse(headers, 403,
new ByteArrayInputStream("{}".getBytes()));
handler = new OAuth2ErrorHandler(new DefaultResponseErrorHandler(), resource);
expected.expect(HttpClientErrorException.class);
handler.handleError(response);
}
@Test
public void testBodyCanBeUsedByCustomHandler() throws Exception {
final String appSpecificBodyContent = "{\"some_status\":\"app error\"}";
OAuth2ErrorHandler handler = new OAuth2ErrorHandler(new ResponseErrorHandler() {
public boolean hasError(ClientHttpResponse response) throws IOException {
return true;
}
public void handleError(ClientHttpResponse response) throws IOException {
InputStream body = response.getBody();
byte[] buf = new byte[appSpecificBodyContent.length()];
int readResponse = body.read(buf);
Assert.assertEquals(buf.length, readResponse);
Assert.assertEquals(appSpecificBodyContent, new String(buf, "UTF-8"));
throw new RuntimeException("planned");
}
}, resource);
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Length", "" + appSpecificBodyContent.length());
headers.set("Content-Type", "application/json");
InputStream appSpecificErrorBody = new ByteArrayInputStream(appSpecificBodyContent.getBytes("UTF-8"));
ClientHttpResponse response = new TestClientHttpResponse(headers, 400, appSpecificErrorBody);
expected.expectMessage("planned");
handler.handleError(response);
}
@Test
public void testHandleErrorWithMissingHeader() throws IOException {
final HttpHeaders headers = new HttpHeaders();
when(response.getHeaders()).thenReturn(headers);
when(response.getStatusCode()).thenReturn(HttpStatus.BAD_REQUEST);
when(response.getBody()).thenReturn(new ByteArrayInputStream(new byte[0]));
when(response.getStatusText()).thenReturn(HttpStatus.BAD_REQUEST.toString());
expected.expect(HttpClientErrorException.class);
handler.handleError(response);
}
// gh-875
@Test
public void testHandleErrorWhenAccessDeniedMessageAndStatus400ThenThrowsUserDeniedAuthorizationException() throws Exception {
String accessDeniedMessage = "{\"error\":\"access_denied\", \"error_description\":\"some error message\"}";
ByteArrayInputStream messageBody = new ByteArrayInputStream(accessDeniedMessage.getBytes());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
ClientHttpResponse response = new TestClientHttpResponse(headers, 400, messageBody);
expected.expect(UserDeniedAuthorizationException.class);
handler.handleError(response);
}
// gh-875
@Test
public void testHandleErrorWhenAccessDeniedMessageAndStatus403ThenThrowsOAuth2AccessDeniedException() throws Exception {
String accessDeniedMessage = "{\"error\":\"access_denied\", \"error_description\":\"some error message\"}";
ByteArrayInputStream messageBody = new ByteArrayInputStream(accessDeniedMessage.getBytes());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
ClientHttpResponse response = new TestClientHttpResponse(headers, 403, messageBody);
expected.expect(OAuth2AccessDeniedException.class);
handler.handleError(response);
}
@Test
public void testHandleMessageConversionExceptions() throws Exception {
HttpMessageConverter<?> extractor = new HttpMessageConverter() {
@Override
public boolean canRead(Class clazz, MediaType mediaType) {
return true;
}
@Override
public boolean canWrite(Class clazz, MediaType mediaType) {
return false;
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return null;
}
@Override
public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
throw new HttpMessageConversionException("error");
}
@Override
public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
}
};
ArrayList<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
messageConverters.add(extractor);
handler.setMessageConverters(messageConverters);
HttpHeaders headers = new HttpHeaders();
final String appSpecificBodyContent = "This user is not authorized";
InputStream appSpecificErrorBody = new ByteArrayInputStream(appSpecificBodyContent.getBytes("UTF-8"));
ClientHttpResponse response = new TestClientHttpResponse(headers, 401, appSpecificErrorBody);
expected.expect(HttpClientErrorException.class);
handler.handleError(response);
}
}